<?php
/**
 * 用户登录[jwt]
 */

namespace app\service;

use app\models\cacheModel\SettingModel;
use app\service\tool\ConvertService;
use app\service\tool\GenerateService;
use yii\helpers\ArrayHelper;

/**
 * jwt[JSON, WEB, TOKEN]登陆类
 * UserLoginModel: Administrator
 * Date: 2018/7/26
 * Time: 17:59
 */
class JwtService extends BaseService
{

    private static $instance = null;
    /**
     * jwt Header JSON 内容
     * @var
     */
    protected $header;
    /**
     * jwt Payload JSON 内容
     * @var
     */
    protected $payload;
    /**
     * jwt Signature 编码 内容
     * @var
     */
    protected $signature;
    /**
     * jwt Secret 密钥 内容
     * @var
     */
    protected $secret = '6313781c8f79301a010c8704';
    /**
     * jwt 内容
     * @var
     */
    protected $jwt;
    /**
     * [Payload]之中某些设置
     * @var array
     */
    protected static $payLoadSetting = [
        'iss'        => null, // 签发人
        'expForever' => 0,   // 过期时间永久值
        'expire'     => 604800,   // 过期时间规定时间，一周7天
        'sub'        => null, // 主题
        'aud'        => null  // 受众
    ];

    /**
     * 自定义载荷[Payload]
     * @var array
     */
    protected $customPayLoad = [];
    /**
     * 具体过期时间
     * @var
     */
    protected $expireTime;

    public function __construct(array $config = [])
    {
        parent::__construct($config);
        // 签发人
        self::$payLoadSetting['iss'] = SettingModel::getSetting('site_name');
        // 主题
        self::$payLoadSetting['sub'] = self::$payLoadSetting['iss'] . '登录token';
        // 受众
        self::$payLoadSetting['aud'] = '数藏集市用户';
    }

    /**
     * 规则验证
     * @return array
     */
    public function rules()
    {

        $parent = parent::rules();

        return ArrayHelper::merge($parent, [
            [[
                'header',
                'payload',
                'signature',
                'secret',
                'jwt',
                'customPayLoad'
            ], 'string'],
            ['expireTime', 'integer']
        ]);
    }

    /**
     * 重写label的 文字
     */
    public function attributeLabels()
    {

        return [
            'customPayLoad' => '自定义载荷'
        ];
    }

    /**
     * 初始化
     * @param array $config [配置选项]
     * @return self
     */
    public static function loadModel($config = [])
    {
        return self::getUploader()->loadConfig($config);
    }

    /**
     * 获取实例
     * @return self
     */
    protected static function getUploader()
    {

        if (empty(self::$instance)) self::$instance = new self();

        return self::$instance;
    }

    /**
     * 载入某些配置
     * @param $config
     * @return self
     */
    public function loadConfig($config)
    {

        $uploader = self::getUploader();
        if (!empty($config['jwt'])) {
            $config = array_merge($config, self::getJwtDecode($config['jwt']));
        }

        foreach ($config as $k => $v) {

            if (!$uploader->hasAttribute($k)) continue;

            $uploader->setAttributes([
                $k => $v
            ], false);
        }

        return $uploader;
    }


    /**
     * 总执行 - 获取新的[jwt]信息
     * @param bool $key 获取的键值，默认全部
     * @return array|mixed|null
     */
    public function getInfo($key = false)
    {

        ### 加载全部
        $this->loadAll();

        $endData = [
            'header'    => $this->header,
            'payload'   => $this->payload,
            'secret'    => $this->secret,
            'signature' => $this->signature,
            'jwt'       => $this->getJwtEncode()
        ];

        if (!$key) return $endData;

        if ($key && is_string($key)) $key = explode('.', $key);
        foreach ($key as $k => $v) {
            if (empty($endData[$v])) return null;
            $endData = $endData[$v];
        }
        return $endData;
    }

    /**
     * 获取[jwt TOKEN]
     * @return string
     */
    public function getToken()
    {

        // 如果[payload]为空
        if (empty($this->payload)) return null;
        // 转json
        $jWTPayload = $this->payload;
        if (ConvertService::isJson($this->payload)) {
            $jWTPayload = json_decode($this->payload, true);
        }
        // [jti]不存在返回
        if (empty($jWTPayload['jti'])) return null;

        return $jWTPayload['jti'];
    }

    /**
     * 获取[jwt EXP]过期时间
     * @return string
     */
    public function getExp()
    {

        // 如果[payload]为空
        if (empty($this->payload)) return null;
        // 转json
        $jWTPayload = $this->payload;
        if (ConvertService::isJson($this->payload)) {
            $jWTPayload = json_decode($this->payload, true);
        }
        // [jti]不存在返回
        if (empty($jWTPayload['exp'])) return null;

        return $jWTPayload['exp'];
    }

    /**
     * 验证[jwt过期时间]是否有效
     * @return string
     */
    public function validateExp()
    {

        // 如果[jwt]不存在直接返回
        if (empty($this->payload)) return false;
        // ******* 1、先获取[payload] *******
        $payload = $this->payload;
        // 如果文本 肯定要转为数组的
        if (is_string($payload)) $payload = json_decode($payload, true);

        // 过期时间不存在返回
        if (empty($payload['exp'])) return false;

        // 是否永久 - 永久返回合法
        if ($payload['exp'] == self::$payLoadSetting['expForever']) return true;
        // 是否过期
        $nowTime = time();
        if ($payload['exp'] < $nowTime) return false;

        // 其他都是合法
        return true;
    }

    /**
     * 验证[jwt签名]是否有效
     * @return string
     */
    public function validateSignature()
    {

        $newSing = $this->generateSignature($this->header, $this->payload);
        return $newSing == $this->signature;
    }

    /**
     * 验证全部
     */
    public function validateAll()
    {

        // 验证时间返回
        if (!$this->validateExp()) {
            $this->addError('exp', '有效期已过');
            return false;
        }
        // 验证时间返回
        if (!$this->validateSignature()) {
            $this->addError('signature', '签名不正确');
            return false;
        }

        return true;
    }

    /**
     * 获取加密的jwt
     * @return string
     */
    public function getJwtEncode()
    {
        return ConvertService::base64UrlEncode($this->loadHeader()) .
            "." .
            ConvertService::base64UrlEncode($this->loadPayload()) .
            "." .
            $this->loadSignature();
    }

    /**
     * 获取解密的jwt
     * @param $jwt
     * @return array
     */
    public function getJwtDecode($jwt)
    {

        $jwt = empty($jwt) ? $this->jwt : $jwt;
        $jwt = explode('.', $jwt);
        if (count($jwt) != 3) return [];
        list($header, $payload, $signature) = $jwt;

        return [
            'header'    => json_decode(
                ConvertService::base64UrlDecode($header), JSON_OBJECT_AS_ARRAY
            ),
            'payload'   => json_decode(
                ConvertService::base64UrlDecode($payload), JSON_OBJECT_AS_ARRAY
            ),
            'signature' => $signature,
        ];
    }


    /**
     * 加载全部所需
     */
    private function loadAll()
    {


        ### 加载[Header]
        $this->loadHeader();
        ### 加载[Payload]
        $this->loadPayload();
        ### 加载[签名]
        $this->loadSignature();

        return $this;
    }

    /**
     * 获取[HEADER]内容
     * @param bool $getNew 强制重新加载
     * @return array|false|string
     */
    private function loadHeader($getNew = false)
    {

        if (!empty($this->header) && is_string($this->header) && !$getNew) {

            return $this->header;
        }

        // [header]数组内容
        if (empty($this->header)) $this->header = [
            'alg' => 'SA259', // 签名的算法（algorithm），默认是 HMAC SHA256（写成 HS256）
            'typ' => 'jwt' // 令牌（token）的类型（type），jwt 令牌统一写为 jwt
        ];

        return $this->generateOther($this->header);
    }

    /**
     * 获取[Payload]内容
     * @param bool $getNew 强制重新加载
     * @return array|false|string
     */
    private function loadPayload($getNew = false)
    {

        if (!empty($this->payload) && is_string($this->payload) && !$getNew) {

            return $this->payload;
        }

        // [Payload]数组内容
        if (empty($this->payload)) {
            $nowTime = time();
            $this->payload = array_merge([
                'iss' => self::$payLoadSetting['iss'], // issuer - 签发人
                'exp' => $this->getExpDeadline(), // expiration time - 过期时间
                'sub' => self::$payLoadSetting['sub'], // subject - 主题
                'aud' => self::$payLoadSetting['aud'], // audience - 受众
                'nbf' => $nowTime, // Not Before - 生效时间
                'iat' => $nowTime, // Issued At - 签发时间
                'jti' => GenerateService::newMongoId() // jwt ID - 编号
            ], $this->customPayLoad);
        }

        return $this->generateOther($this->payload);
    }

    /**
     * 获取[Signature]签名内容
     * @param bool $getNew 强制重新加载
     * @return string
     */
    private function loadSignature($getNew = false)
    {

        // 存在签名即可返回
        if (!empty($this->signature) && !$getNew) {

            return $this->signature;
        }

        return $this->generateSignature($this->header, $this->payload);
    }

    /**
     * 生成签名内容
     * @param $header
     * @param $payload
     * @return string
     */
    private function generateSignature($header, $payload) {

        /**
         * jwt 作为一个令牌（token），有些场合可能会放到 URL
         * （比如 api.example.com/?token=xxx）。Base64 有三个字符
         * + 、 / 和 = ，在 URL 里面有特殊含义，
         * 所以要被替换掉： = 被省略、 + 替换成 - ， / 替换成 _ 。
         * 这就是 Base64URL 算法
         */
        $hashData =
            ConvertService::base64UrlEncode($this->generateOther($header)) .
            "." .
            ConvertService::base64UrlEncode($this->generateOther($payload));

        // 用 Header 里面指定的签名算法（默认是 HMAC SHA256）
        return hash_hmac('sha256', $hashData, $this->secret);
    }

    /**
     * 生成其他内容
     * @param $data
     * @return false|string
     */
    public function generateOther($data) {

        if (is_array($data)) {
            $data = json_encode($data, JSON_UNESCAPED_UNICODE);
        }
        return $data;
    }

    /**
     * 获取[Payload]中的[exp] - [exp]过期时间 自定义
     * @param int $getTime 获取时间
     * @return mixed
     */
    private function getExpDeadline($getTime = false)
    {

        if ($this->expireTime) return $this->expireTime;
        if ($getTime === false) $getTime = time();
        return bcadd($getTime, self::$payLoadSetting['expire']);
    }
}
